yarn Berry는 npm의 문제를 어떻게 해결했을까

npm과 Yarn v1은 모두 node_modules 구조를 직접 생성한다. 이 구조는 다음과 같은 문제를 낳았다: 프로젝트마다 중복된 모듈이 수천 개씩 설치됨 CI에서 node_modules 캐싱이 비효율적 (특히 대규모 모노레포에서) 플랫폼 간 path 길이 제한 문제 (Windows 등)

느린 설치 속도 문제를 어떻게 해결했을까? : PlugNPlay

PnP는 “설치하면 곧바로 실행 가능(Plug → Play)”를 가능하게하는 yarn berry의 기능이다.

npm과 yarn은 네트워크를 통해 패키지를 다운로드 하고 압축을 풀고 node_modules에 배치한다.
Yarn Berry는 “node_modules가 필요 없는 세상”을 제시했다.
“굳이 매번 압축을 풀어 수천 개의 파일을 node_modules에 복사할 필요가 있을까?”
yarn berry는 node_modules를 없애고, 패키지들을 .yarn/cache폴더에 다운 받은 zip 형태 그대로 저장한다.

.yarn/cache/
  ├── react-npm-18.2.0.zip
  ├── lodash-npm-4.17.21.zip
  └── axios-npm-1.6.2.zip
.pnp.cjs

압축은 그대로 둔 채, Yarn이 생성한 pnp.cjs파일에
“이 zip 파일 안에 뭐가 들어있는지, 어떤 패키지가 어디에 있는지”를 전부 매핑해둔다.

그럼 어떻게 실행될까? — pnp.cjs

Node.js의 require는 모듈을 불러올 때
파일시스템에서 node_modules/react/index.js 파일을 찾는다.

yarn berry는 require를 커스터마이징 해서. pnp.cjs로 찾아가게 만든다.

  1. “react? .yarn/cache/react-npm-18.2.0.zip 안에 있어”
  2. ”거기 zip 내부 경로 /node_modules/react/index.js 파일을 읽으면 돼”를 알려준다.
  3. 그리고 Node는 zip 안의 파일을 바로 읽어서 실행한다.

뭔가를 사용하려면 압축을 해제해야만 쓸 수 있는거 아닐까?

보통 우리가 .zip 파일을 연다고 하면, [압축 해제(unzip) -> 파일 시스템에 풀고 -> 그걸 읽는다.] 의 흐름으로 생각한다.

즉 “한 번 압축을 풀어야 접근할 수 있다”는 전제를 가지고 있는데, 압축 해제는 I/O + CPU 연산이기 때문에 당연히 느릴 수밖에 없다. 그런데 이건 어플리케이션 사용할 때 유저 관점에서의 생각이다.

ZIP 파일은 구조적으로 “랜덤 액세스(random access)”가 가능하다. 즉, ZIP 포맷은 맨 뒤쪽(EOF)에 ‘파일 인덱스(central directory)’를 가지고 있어서 파일의 정확한 위치(offset)를 빠르게 찾고 fs.read()로 읽을 수 있다.

node_modules가 없어지므로 설치 속도, 디스크 공간 절약하고 의존성을 찾는 것도 빠르다.


Yarn Berry의 PnP는 어떻게 유령의존성을 해결했는가

의존성 중복 설치를 막기 위해 Flatten 방식을 도입했고, 그 결과 Upward로 탐색하는 require의 동작에 의해 유령의존성 문제가 발생했다.

Yarn Berry는 이 구조 자체를 완전히 바꿔버렸다.

  • Node의 기본 모듈 탐색 방식을 오버라이드하여
    require()가 파일시스템을 탐색하지 않고 pnp.cjs를 참조하도록 만들었다.
  • pnp.cjs에는 모든 패키지와 그 하위 의존성을
    MAP 형태(“A → B, C, D”) 로 정리해둔 의존성 그래프가 기록된다.
  • 따라서 어떤 모듈을 require()할 때,
    Yarn은 파일 경로가 아니라 이 매핑 테이블에서 정확한 대상 패키지를 찾아준다.

이 구조에서는 node_modules 디렉터리도 없고, 평탄화나 호이스팅이 존재하지 않으며, 상위 폴더를 탐색할 일도 없다.
즉, 유령 의존성이 발생하지 않는다.


Zero-Install ?

Zero-Install은 .yarn/cache와 .pnp.cjs를 Git에 커밋해서,
팀원이나 CI가 yarn install을 실행할 필요조차 없게 만드는 방식을 말한다.

Zero Install은 Yarn berry만의 특징은 아니다.
모든 의존성을 형상관리 시스템에 올리면 어느 패키지매니저나 zero-install이 가능하다.
다만 yarn berry는 의존성들을 압축파일로 들고있기 때문에, Zero-Install에 적합한 것 뿐이다.

이 Zero-Install을 이용하여 팀원이나 CI가 yarn install을 실행할 필요조차 없게할 수 있다.
항상 동일한 환경을 보장할 수 있다.


nodeLinker: node-modules

yarn berry를 쓰다보면, 어떤 라이브러리는 yarn berry에서 쓸 수 없다고도 한다. 그래서 nodeLinker : node-modules로 바꿔 결국 node_modules 폴더를 살려둬야 할 때가 있다.

도대체 왜 어떤 라이브러리는 nodeLinker: node-modules 안 하면 안 되냐?
보통 오래된 버전의 의존성이 있는 프로젝트에서 yarn berry를 활용하면 이런 일이 발생한다.
postinstall를 돌려야 되거나, fs로 자기 의존성 패키지에 접근하는 라이브러리가 있는 경우에
yarn berry에는 node_modules가 없어 실행이 안된다.

유령의존성 문제

nodeLinker : pnp 는 .yarn/cache 폴더 밑에 .zip으로 저장하고 커스텀된 require를 통해 의존성을 찾는다. nodeLinker : node-modules는 .yarn/cache에 zip파일로 다운은 받아놓고 풀어서 nodeModules에 복제하고 기존의 require를 사용한다. 그리고 pnp.cjs파일은 없다.

그렇다면 결국 다시 유령의존성 문제/의존성 충돌 문제가 발생할 수 있지 않을까 ?
npm의 문제를 다시보면, 결국 유령의존성은 의존성 중복 설치를 막기 위해 Flatten 방식을 도입했고, 그 결과 Upward로 탐색하는 require의 동작에 의해 발생하는 것이었다.

yarn berry는 평탄화를 아무거나 하진 않는다.
동일 버전의 의존성만 평탄화하여, 아무거나 막 호이스팅하지 않았기 때문에, yarn berry의 때문에 의존성 충돌 문제가 발생할 일은 없다.
다만, 유령 의존성 문제는 발생할 가능성이 있다.